/**
 * Copyright 2013 The FROG Brigade
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.frogbrigade.bufonidae.io;

import java.io.IOException;
import java.io.Reader;
import java.nio.CharBuffer;

/**
 * 入力ストリームをCSV（Character Separated Values）として読み進めるリーダーです。
 * <p>
 * リーダーが読み進めることができるCSVの仕様は『<a href="http://www.ietf.org/rfc/rfc4180.txt">RFC 4180</a>』を基にしていますが、次の5点が異なります。
 * <ol>
 * <li>フィールドの区切り文字にカンマ（0x2c）以外の文字を指定できます。但し、レコードの区切り文字やフィールドの囲み文字とは、異なる必要があります。</li>
 * <li>フィールドの囲み文字にダブルクォーテーション（0x22）以外の文字を指定できます。但し、レコードの区切り文字やフィールドの区切り文字とは、異なる必要があります。</li>
 * <li>レコードの区切り文字としてCRLF以外にCR、LF、LFCRも解析します。</li>
 * <li>ヘッダーとレコードのフィールド数の矛盾は、無視します。</li>
 * <li>フィールドの区切り文字でレコードが終了する場合は、エラーとせずに最後のフィールドを空として解析します。</li>
 * </ol>
 * 
 * @author nobuyuki sekimori
 * @see <a href="http://www.ietf.org/rfc/rfc4180.txt">RFC4180</a>
 * @since 1.0.0
 * @version 1.0.0
 */
public class CsvReader
        extends Reader
{
    private static final int CR                = 0x0d;
    private static final int LF                = 0x0a;
    private static final int DEFAULT_DELIMITER = 0x2c;
    private static final int DEFAULT_QUOTE     = 0x22;
    private static final int END_OF_STREAM     = -1;
    private static final int END_OF_RECORD     = -2;
    private static final int END_OF_FIELD      = -3;

    private Reader           in                = null;
    private long             pos               = 0L;
    private int[]            cpBuf             = new int[] { CsvReader.END_OF_STREAM, CsvReader.END_OF_STREAM };
    private boolean          isInQuotes        = false;
    private long             markedPos         = 0L;
    private int[]            markedCpBuf       = new int[] { CsvReader.END_OF_STREAM, CsvReader.END_OF_STREAM };
    private boolean          markedIsInQuotes  = false;
    private int              delimiter         = CsvReader.DEFAULT_DELIMITER;
    private int              quote             = CsvReader.DEFAULT_QUOTE;

    /**
     * CSVとして読み進める入力ストリームを含むリーダーを指定してオブジェクトを生成します。
     * <p>
     * 区切文字と囲み文字は、それぞれディフォルトのカンマ（0x2c）とダブルクォーテーション（0x22）を使用します。
     * 
     * @param in
     *            a Reader.
     */
    public CsvReader(Reader in)
    {
        this(in, (char) CsvReader.DEFAULT_DELIMITER, (char) CsvReader.DEFAULT_QUOTE);
    }

    /**
     * CSVとして読み進める入力ストリームを含むリーダー、区切文字と囲み文字を指定してオブジェクトを生成します。
     * <p>
     * 区切り文字と囲み文字に同じ文字、改行文字を指定すると、IllegalArgumentExceptionをスローします。
     * 
     * @param in
     *            a Reader.
     * @param delimiter
     *            a field delimiter.
     * @param quote
     *            a field quotes.
     */
    public CsvReader(Reader in, char delimiter, char quote)
    {
        super(in);

        if (delimiter == quote) { throw new IllegalArgumentException("same character is specified in delimiter and quote."); }
        if (delimiter == CsvReader.CR || delimiter == CsvReader.LF) { throw new IllegalArgumentException("CR or LF are specified in delimiter."); }
        if (quote == CsvReader.CR || quote == CsvReader.LF) { throw new IllegalArgumentException("CR or LF are specified in quote."); }

        this.in = in;
        this.delimiter = delimiter;
        this.quote = quote;
    }

    /**
     * CSVの区切文字を取得します。
     * 
     * @return the field delimiter.
     */
    public char getDelimiter()
    {
        return (char) this.delimiter;
    }

    /**
     * CSVの囲み文字を取得します。
     * 
     * @return the field quotes.
     */
    public char getQuote()
    {
        return (char) this.quote;
    }

    /**
     * 入力ストリームを1文字読み進め、読み込みバッファへ格納します。
     * <p>
     * CsvReader内で入力ストリームを読み進める場合には、必ずこのメソッドを介す必要があります。
     * 
     * @throws IOException
     *             If an I/O error occurs.
     */
    private void readAhead()
            throws IOException
    {
        this.cpBuf[0] = this.cpBuf[1];
        this.cpBuf[1] = this.in.read();

        if (this.cpBuf[1] != CsvReader.END_OF_STREAM)
        {
            this.pos++;
        }
    }

    /**
     * 入力ストリームをCSVとして1文字読み進めます。
     * 
     * @return 読み込んだ1文字の0〜65535（0x00-0xffff）の範囲の整数表現です。
     *         <p>
     *         但し、入力ストリームの終わりに達した場合は-1、各レコードの終わりに達した場合は-2、各フィールドの終わりに達した場合は-3
     *         を返します。 各フィールドの囲み文字と区切り文字、各レコードの切れ目を表す改行文字は返されません。
     * @throws IOException
     *             If an I/O error occurs or an illegal format is detected.
     */
    @Override
    public int read()
            throws IOException
    {
        synchronized (this.lock)
        {
            this.readAhead();

            if (!this.isInQuotes)
            {
                if (this.cpBuf[0] == CsvReader.CR && this.cpBuf[1] == CsvReader.LF || this.cpBuf[0] == CsvReader.LF && this.cpBuf[1] == CsvReader.CR)
                {
                    this.readAhead();
                }

                if (this.cpBuf[1] == this.quote)
                {
                    if (this.cpBuf[0] == CsvReader.END_OF_STREAM || this.cpBuf[0] == this.delimiter || this.cpBuf[0] == CsvReader.CR
                        || this.cpBuf[0] == CsvReader.LF)
                    {
                        this.isInQuotes = true;
                        this.readAhead();
                    }
                    else
                    {
                        throw new IOException("illegal quotation mark is detected.");
                    }
                }
            }

            if (this.isInQuotes && this.cpBuf[1] == this.quote)
            {
                this.readAhead();

                if (this.cpBuf[1] == this.delimiter || this.cpBuf[1] == CsvReader.CR || this.cpBuf[1] == CsvReader.LF
                    || this.cpBuf[1] == CsvReader.END_OF_STREAM)
                {
                    this.isInQuotes = false;
                }
                else
                {
                    if (this.cpBuf[1] != this.quote) { throw new IOException("quotation mark not escaped is detected."); }
                }
            }

            if (!this.isInQuotes)
            {
                if (this.cpBuf[1] == this.delimiter) { return CsvReader.END_OF_FIELD; }
                if (this.cpBuf[1] == CsvReader.CR || this.cpBuf[1] == CsvReader.LF) { return CsvReader.END_OF_RECORD; }
            }

            return this.cpBuf[1];
        }
    }

    /**
     * 入力ストリームをCSVとして指定した配列の長さ分読み進め、指定した配列の初めから格納します。
     * 
     * @param cbuf
     *            読み込んだ文字を格納する配列です。
     * @return 読み込んだ文字数です。
     *         <p>
     *         但し、入力ストリームの終わりに達した場合は-1を返します。
     * @exception IOException
     *                If an I/O error occurs.
     */
    @Override
    public int read(char[] cbuf)
            throws IOException
    {
        synchronized (this.lock)
        {
            return this.read(cbuf, 0, cbuf.length);
        }
    }

    /**
     * 入力ストリームをCSVとして指定した文字数分読み進め、指定した配列に指定した位置から格納します。
     * 
     * @param cbuf
     *            読み込んだ文字を格納する配列です。
     * @param off
     *            読み込んだ文字の格納を開始する配列の位置です。
     * @param len
     *            読み進める文字数です。
     * @return 読み込んだ文字数です。
     *         <p>
     *         但し、入力ストリームの終わりに達した場合は-1を返します。
     * @throws IOException
     *             入出力エラーが発生した場合。
     *             <p>
     *             又は、指定した配列の長さと位置と文字数の関係に不整合がある場合、入力ストリームがCSVとして解析できないフォーマットの場合
     *             。
     */
    @Override
    public int read(char[] cbuf, int off, int len)
            throws IOException
    {
        synchronized (this.lock)
        {
            if (off < 0 || off > cbuf.length || len < 0 || off + len > cbuf.length || off + len < 0) { throw new IndexOutOfBoundsException(); }

            int cbufIndex = 0;

            for (cbufIndex = 0; cbufIndex < len; cbufIndex++)
            {
                int cp = this.read();

                if (cp == CsvReader.END_OF_STREAM)
                {
                    break;
                }

                cbuf[off + cbufIndex] = (char) cp;
            }

            return len > 0 && cbufIndex == 0 ? CsvReader.END_OF_STREAM : cbufIndex;
        }
    }

    /**
     * 入力ストリームをCSVとして指定したバッファの残り要素数分読み進め、指定したバッファの現在位置から格納します。
     * 
     * @param target
     *            読み込んだ文字を格納するバッファです。
     * @return 読み込んだ文字数です。
     *         <p>
     *         但し、入力ストリームの終わりに達した場合は-1を返します。
     * @throws IOException
     *             If an I/O error occurs.
     */
    @Override
    public int read(CharBuffer target)
            throws IOException
    {
        synchronized (this.lock)
        {
            int len = target.remaining();
            char[] cbuf = new char[len];
            int n = this.read(cbuf, 0, len);

            if (n > 0)
            {
                target.put(cbuf, 0, n);
            }

            return n;
        }
    }

    /**
     * 入力ストリームをCSVとして1レコード分のフィールドを読み進めます。
     * 
     * @return CSVの1レコード分のフィールドです。
     *         <p>
     *         但し、入力ストリームの終わりに達した場合はnullを返します。
     * @throws IOException
     *             入出力エラーが発生した場合。
     */
    public String[] readRecord()
            throws IOException
    {
        synchronized (this.lock)
        {
            String[] recordBuf = new String[16];
            int recordBufIndex = 0;
            char[] fieldBuf = new char[16];
            int fieldBufIndex = 0;

            while (true)
            {
                int cp = this.read();

                if (cp == CsvReader.END_OF_STREAM || cp == CsvReader.END_OF_RECORD || cp == CsvReader.END_OF_FIELD)
                {
                    if (cp == CsvReader.END_OF_STREAM && recordBufIndex == 0 && fieldBufIndex == 0)
                    {
                        break;
                    }

                    String field = new String(fieldBuf, 0, fieldBufIndex);

                    if (recordBufIndex == recordBuf.length)
                    {
                        String[] tmpField = new String[recordBuf.length * 2];
                        System.arraycopy(recordBuf, 0, tmpField, 0, recordBuf.length);
                        recordBuf = tmpField;
                    }

                    recordBuf[recordBufIndex] = field;
                    recordBufIndex++;
                    fieldBufIndex = 0;

                    if (cp == CsvReader.END_OF_STREAM || cp == CsvReader.END_OF_RECORD)
                    {
                        break;
                    }

                    continue;
                }

                if (fieldBufIndex == fieldBuf.length)
                {
                    char[] tmpFieldBuf = new char[fieldBuf.length * 2];
                    System.arraycopy(fieldBuf, 0, tmpFieldBuf, 0, fieldBuf.length);
                    fieldBuf = tmpFieldBuf;
                }

                fieldBuf[fieldBufIndex] = (char) cp;
                fieldBufIndex++;
            }

            if (recordBufIndex > 0)
            {
                String[] record = new String[recordBufIndex];
                System.arraycopy(recordBuf, 0, record, 0, recordBufIndex);

                return record;
            }

            return null;
        }
    }

    /**
     * 入力ストリームをCSVとして指定した文字数スキップします。
     * <p>
     * スキップする文字数には、フィールドの値となる文字、フィールドの区切文字、フィールドの改行文字（2文字で改行を示す場合には1文字としてカウント）
     * が含まれます。 フィールドの囲み文字、フィールドの値となる囲み文字のエスケープシーケンスは、スキップする文字数に含まれません。
     * 但し、戻り値の実際にスキップした文字数には 、フィールドの囲み文字、フィールドの値となる囲み文字のエスケープシーケンス、フィールドの改行文字（2
     * 文字で改行を示す場合には2文字としてカウント ）が含まれます。
     * 
     * @param n
     *            スキップする文字数です。
     * @return 実際にスキップした文字数です。
     * @throws IOException
     *             If an I/O error occurs.
     */
    @Override
    public long skip(long n)
            throws IOException
    {
        if (n < 0L) { throw new IllegalArgumentException("negative value is specified in number of characters to skip."); }

        synchronized (this.lock)
        {
            long startPos = this.pos;

            for (int i = 0; i < n; i++)
            {
                if (this.read() == CsvReader.END_OF_STREAM)
                {
                    break;
                }
            }

            return this.pos - startPos;
        }
    }

    /**
     * CSVとして読み進める入力ストリームが読み込み可能かどうか判定します。
     * 
     * @return 次のread()呼び出しでCSVとして読み進める入力ストリームが確実に読み込み可能かどうかです。
     *         <p>
     *         falseが返されても、読み込みが絶対にブロックされるわけではありません。
     * @throws IOException
     *             If an I/O error occurs.
     */
    @Override
    public boolean ready()
            throws IOException
    {
        synchronized (this.lock)
        {
            return super.ready();
        }
    }

    /**
     * CSVとして読み進める入力ストリームがmark()をサポートするかどうか判定します。
     * 
     * @return CSVとして読み進める入力ストリームがmark()をサポートするかどうかです。
     */
    @Override
    public boolean markSupported()
    {
        return this.in.markSupported();
    }

    /**
     * CSVとして読み進める入力ストリームの現在位置にマークを設定します。
     * <p>
     * マークの設定以降にreset()を呼び出すと、この位置へ入力ストリームをリセットします。
     * 
     * @param readAheadLimit
     *            読み込んだ文字を格納する配列です。
     * @throws IOException
     *             入出力エラーが発生した場合。
     *             <p>
     *             又は、CSVとして読み進める入力ストリームがmark()をサポートしていない場合。
     */
    @Override
    public void mark(int readAheadLimit)
            throws IOException
    {
        if (readAheadLimit < 0) { throw new IllegalArgumentException("negative value is specified in read ahead limit."); }

        synchronized (this.lock)
        {
            this.in.mark(readAheadLimit);
            this.markedPos = this.pos;
            this.markedCpBuf[0] = this.cpBuf[0];
            this.markedCpBuf[1] = this.cpBuf[1];
            this.markedIsInQuotes = this.isInQuotes;
        }
    }

    /**
     * CSVとして読み進める入力ストリームをリセットします。
     * <p>
     * 入力ストリームにマークが設定されている場合は、その位置へ入力ストリームをリセットします。
     * 
     * @throws IOException
     *             入出力エラーが発生した場合。
     *             <p>
     *             又は、CSVとして読み進める入力ストリームがreset()をサポートしていない場合。
     */
    @Override
    public void reset()
            throws IOException
    {
        synchronized (this.lock)
        {
            this.in.reset();
            this.pos = this.markedPos;
            this.cpBuf[0] = this.markedCpBuf[0];
            this.cpBuf[1] = this.markedCpBuf[1];
            this.isInQuotes = this.markedIsInQuotes;
        }
    }

    /**
     * CSVとして読み進める入力ストリームを閉じ、関連するすべてのシステムリソースを解放します。
     * <p>
     * 閉じた後にclose以外の入力ストリームを操作するメソッドを呼び出すと、IOExceptionをスローします。
     * 
     * @throws IOException
     *             If an I/O error occurs.
     */
    @Override
    public void close()
            throws IOException
    {
        synchronized (this.lock)
        {
            if (this.in != null)
            {
                this.in.close();
            }

            this.in = null;
            this.pos = 0L;
            this.cpBuf = new int[] { CsvReader.END_OF_STREAM, CsvReader.END_OF_STREAM };
            this.isInQuotes = false;
        }
    }
}
